Skip to content

[google_maps_flutter] Convert heatmap controller to Swift - #12713

Open
stuartmorgan-g wants to merge 17 commits into
flutter:mainfrom
stuartmorgan-g:maps-swift-heatmaps-and-utils
Open

[google_maps_flutter] Convert heatmap controller to Swift#12713
stuartmorgan-g wants to merge 17 commits into
flutter:mainfrom
stuartmorgan-g:maps-swift-heatmaps-and-utils

Conversation

@stuartmorgan-g

Copy link
Copy Markdown
Collaborator

This converts HeatmapController and the remaining utility functions to Swift in the _sdk* packages.

The final remaining Obj-C code will be migrated in a follow-up PRs.

The conversion process was:

  • Initial conversion via Gemini, with explicit instruction to keep the structure the same.
  • Side-by-side manual review of the old and new versions of the files.
  • Manual fixes and improvements.

The conversion code utils have much more change than previous PRs, since the direct conversion code felt very non-idiomatic in Swift. Almost all of the free functions for Pigeon<->Maps SDK type conversions were converted to extensions on the Pigeon types:

  • To avoid the fragile pattern of putting extension methods on types we don't control (which can lead to collisions), all the extensions are on the Pigeon types, and so the APIs aren't symmetrical:
    • Maps -> Pigeon is done via a convenience constructor (generally called make(from:)
    • Pigeon -> Maps is done via a toMapsSDKClassName() method on the Pigeon type (I'm not sold on that naming pattern; alternate suggestions welcome)
  • Conversions functions that were array-based have been changed to single-element conversions following the pattern above, and then the call sites changed to just map that conversion function, since map is a simple and idiomatic pattern in Swift, unlike the loop-and-add construction that had been required in Obj-C.
    • This in turn caused some tests to be simplified, since we didn't need to test things like the number of list items converted, and can instead just test the individual conversion.

The test bridging header is removed since there are no longer any _Test headers.

Part of flutter/flutter#119108

Pre-Review Checklist

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. 2

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request converts the heatmap controller and data conversion logic from Objective-C to Swift across the iOS packages (sdk9, sdk10, and shared_code), removing several Objective-C files in favor of Swift implementations. The review feedback highlights critical safety issues, pointing out potential runtime crashes from unsafely unwrapping optional values like heatmap.gradient and heatmap.weightedData. Additionally, the reviewer suggests performance optimizations, such as avoiding unnecessary CLLocation heap allocations by passing CLLocationCoordinate2D directly to path-creation helpers, and refactoring redundant image loading logic in ImageUtils.swift.

Comment on lines +89 to +97
var gmsMapViewType: GMSMapViewType {
switch self {
case .none: return .none
case .normal: return .normal
case .satellite: return .satellite
case .terrain: return .terrain
case .hybrid: return .hybrid
@unknown default: return .normal
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this being a var, is it set once and then never changed? Is it possible for the map type to change?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a var because that's the syntax for a computed property; because there's no set defined for it, it's not actually assignable. FGMPlatformMapType is an enum, so an individual instance of it can't change value.

self.heatmapTileLayer = tileLayer
self.mapView = mapView
super.init()
HeatmapController.update(tileLayer, from: heatmap, mapView: mapView)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this need to pass self.tileLayer?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.heatmapTypeLayer and tileLayer are the same at this point, so it doesn't actually matter which is passed.


var image: UIImage?

switch bitmap {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add back comment

Suggested change
switch bitmap {
// See comment in messages.dart for why this is so loosely typed. See also
// https://github.com/flutter/flutter/issues/117819.
switch bitmap {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. I caught that in the conversion utils, but missed it here.

(Also, good news: this problem is fixed in the next PR, since Swift Pigeon supports a limited form of class hierarchy for this use case.)

case let bitmap as FGMPlatformBitmapBytes:
// Deprecated: This message handling for 'fromBytes' has been replaced by 'bytes'.
// Refer to the flutter google_maps_flutter_platform_interface package for details.
image = UIImage(data: bitmap.byteData.data, scale: screenScale)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the try catch not necessary here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't catch NSExceptions in Swift, so if UIImage's constructor can leak NSExceptions in current versions of iOS, Apple has done something very wrong 🙂

A Swift do/catch would catch Swift errors, but this constructor can't throw those because it's not marked as throws.

}
case let bitmap as FGMPlatformBitmapBytesMap:
let bytes = bitmap.byteData
image = UIImage(data: bytes.data, scale: screenScale)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try catch not required?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

@vashworth

Copy link
Copy Markdown
Contributor

Some tests are failing

00:13 +33 ~5: GroundOverlay set ground overlays correctly
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following _TypeError was thrown running a test:
type 'int' is not a subtype of type 'double?' in type cast

When the exception was thrown, this was the stack:
#0      PlatformBitmapDefaultMarker.decode (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:2375:55)
#1      _PigeonCodec.readValueOfType (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:2997:44)
#2      StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:480:12)
#3      StandardMessageCodec.readValueOfType (package:flutter/src/services/message_codecs.dart:525:23)
#4      _PigeonCodec.readValueOfType (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:3011:22)
#5      StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:480:12)
#6      _PigeonCodec.readValueOfType (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:2995:38)
#7      StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:480:12)
#8      StandardMessageCodec.readValueOfType (package:flutter/src/services/message_codecs.dart:525:23)
#9      _PigeonCodec.readValueOfType (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:3011:22)
#10     StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:480:12)
#11     _PigeonCodec.readValueOfType (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:2979:45)
#12     StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:480:12)
#13     StandardMessageCodec.readValueOfType (package:flutter/src/services/message_codecs.dart:525:23)
#14     _PigeonCodec.readValueOfType (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:3011:22)
#15     StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:480:12)
#16     StandardMessageCodec.decodeMessage (package:flutter/src/services/message_codecs.dart:342:28)
#17     BasicMessageChannel.send (package:flutter/src/services/platform_channel.dart:241:18)
<asynchronous suspension>
#18     MapsInspectorApi.getGroundOverlayInfo (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:4216:33)
<asynchronous suspension>
#19     GoogleMapsInspectorIOS.getGroundOverlayInfo (package:google_maps_flutter_ios_sdk10/src/google_map_inspector_ios.dart:107:54)
<asynchronous suspension>
#20     main.<anonymous closure>.<anonymous closure> (file:///Volumes/Work/s/w/ir/x/w/packages/packages/google_maps_flutter/google_maps_flutter_ios_sdk10/example/integration_test/google_maps_test.dart:1602:57)
<asynchronous suspension>
#21     testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:192:15)
<asynchronous suspension>
#22     TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1980:5)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)

The test description was:
  set ground overlays correctly
════════════════════════════════════════════════════════════════════════════════════════════════════
00:16 +33 ~5 -1: GroundOverlay set ground overlays correctly [E]
  Test failed. See exception logs above.
  The test description was: set ground overlays correctly
  
00:16 +33 ~5 -1: GroundOverlay update ground overlays correctly
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following _TypeError was thrown running a test:
type 'int' is not a subtype of type 'double?' in type cast

When the exception was thrown, this was the stack:
#0      PlatformBitmapDefaultMarker.decode (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:2375:55)
#1      _PigeonCodec.readValueOfType (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:2997:44)
#2      StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:480:12)
#3      StandardMessageCodec.readValueOfType (package:flutter/src/services/message_codecs.dart:525:23)
#4      _PigeonCodec.readValueOfType (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:3011:22)
#5      StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:480:12)
#6      _PigeonCodec.readValueOfType (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:2995:38)
#7      StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:480:12)
#8      StandardMessageCodec.readValueOfType (package:flutter/src/services/message_codecs.dart:525:23)
#9      _PigeonCodec.readValueOfType (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:3011:22)
#10     StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:480:12)
#11     _PigeonCodec.readValueOfType (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:2979:45)
#12     StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:480:12)
#13     StandardMessageCodec.readValueOfType (package:flutter/src/services/message_codecs.dart:525:23)
#14     _PigeonCodec.readValueOfType (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:3011:22)
#15     StandardMessageCodec.readValue (package:flutter/src/services/message_codecs.dart:480:12)
#16     StandardMessageCodec.decodeMessage (package:flutter/src/services/message_codecs.dart:342:28)
#17     BasicMessageChannel.send (package:flutter/src/services/platform_channel.dart:241:18)
<asynchronous suspension>
#18     MapsInspectorApi.getGroundOverlayInfo (package:google_maps_flutter_ios_sdk10/src/messages.g.dart:4216:33)
<asynchronous suspension>
#19     GoogleMapsInspectorIOS.getGroundOverlayInfo (package:google_maps_flutter_ios_sdk10/src/google_map_inspector_ios.dart:107:54)
<asynchronous suspension>
#20     main.<anonymous closure>.<anonymous closure> (file:///Volumes/Work/s/w/ir/x/w/packages/packages/google_maps_flutter/google_maps_flutter_ios_sdk10/example/integration_test/google_maps_test.dart:1675:57)
<asynchronous suspension>
#21     testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:192:15)
<asynchronous suspension>
#22     TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1980:5)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)

The test description was:
  update ground overlays correctly
════════════════════════════════════════════════════════════════════════════════════════════════════
00:19 +33 ~5 -2: GroundOverlay update ground overlays correctly [E]
  Test failed. See exception logs above.
  The test description was: update ground overlays correctly
  

@stuartmorgan-g stuartmorgan-g left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some tests are failing

Ha. This is a fun little reminder of why we should be moving to Swift, where we have strong uniform typing 🙂

FGMPlatformBitmap *placeholderImage =
    [FGMPlatformBitmap makeWithBitmap:[FGMPlatformBitmapDefaultMarker makeWithHue:0]];
let placeholderImage = FGMPlatformBitmap.make(
  withBitmap: FGMPlatformBitmapDefaultMarker.make(withHue: 0))

Looks the same, right? Only, because hue is a nullable double in the Pigeon definition, the Obj-C FGMPlatformBitmap has to declare it as an NSNumber*, not a double. So in the Obj-C, 0 was actually a misspelling of nil... which didn't matter, because they are both just integers.

But in Swift, 0 is an integer in an NSNumber context, so is equivalent to the Obj-C @(0), and since the 0 is an int, it gets encoded with the wrong type, and so explodes on the Dart side.

Fixed by correcting the spelling of nil. (The potential for this kind of problem is eliminated in the next PR, when all of these data classes become Swift.)

Comment on lines +89 to +97
var gmsMapViewType: GMSMapViewType {
switch self {
case .none: return .none
case .normal: return .normal
case .satellite: return .satellite
case .terrain: return .terrain
case .hybrid: return .hybrid
@unknown default: return .normal
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a var because that's the syntax for a computed property; because there's no set defined for it, it's not actually assignable. FGMPlatformMapType is an enum, so an individual instance of it can't change value.

self.heatmapTileLayer = tileLayer
self.mapView = mapView
super.init()
HeatmapController.update(tileLayer, from: heatmap, mapView: mapView)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.heatmapTypeLayer and tileLayer are the same at this point, so it doesn't actually matter which is passed.


var image: UIImage?

switch bitmap {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. I caught that in the conversion utils, but missed it here.

(Also, good news: this problem is fixed in the next PR, since Swift Pigeon supports a limited form of class hierarchy for this use case.)

case let bitmap as FGMPlatformBitmapBytes:
// Deprecated: This message handling for 'fromBytes' has been replaced by 'bytes'.
// Refer to the flutter google_maps_flutter_platform_interface package for details.
image = UIImage(data: bitmap.byteData.data, scale: screenScale)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't catch NSExceptions in Swift, so if UIImage's constructor can leak NSExceptions in current versions of iOS, Apple has done something very wrong 🙂

A Swift do/catch would catch Swift errors, but this constructor can't throw those because it's not marked as throws.

}
case let bitmap as FGMPlatformBitmapBytesMap:
let bytes = bitmap.byteData
image = UIImage(data: bytes.data, scale: screenScale)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants